package com.joshjung.echosurf
{
    import com.joshjung.echosurf.event.ESCAuthenticateEvent;
    import com.joshjung.echosurf.event.ESJoinEvent;
    import com.joshjung.echosurf.event.ESLeaveEvent;
    import com.joshjung.echosurf.event.ESPacketEvent;
    import com.joshjung.echosurf.event.ESServerEvent;

    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IOErrorEvent;
    import flash.events.ProgressEvent;
    import flash.net.Socket;
    import flash.utils.ByteArray;
    import flash.utils.Dictionary;
    import flash.utils.Endian;

    [Event(name = "connect", type = "com.joshjung.echosurf.event.ESCAuthenticateEvent")]
    [Event(name = "peerJoin", type = "com.joshjung.echosurf.event.ESJoinEvent")]
    [Event(name = "peerLeave", type = "com.joshjung.echosurf.event.ESLeaveEvent")]
    [Event(name = "startServer", type = "com.joshjung.echosurf.event.ESServerEvent")]
    [Event(name = "serverChange", type = "com.joshjung.echosurf.event.ESServerEvent")]
    [Event(name = "packetReceived", type = "com.joshjung.echosurf.event.ESPacketEvent")]

    public class ESSocket extends EventDispatcher
    {
        //---------------------------------------------------------------------
        //
        //  Static Variables
        //
        //---------------------------------------------------------------------
        public static const STATE_INIT : int = 0;
        public static const STATE_STATUS_BYTE : int = 1;
        public static const STATE_PEER_DEATH : int = 2;
        public static const STATE_SERVER_DEATH : int = 3;
        public static const STATE_PACKET_HEADER : int = 4;
        public static const STATE_PACKET : int = 5;

        //---------------------------------------------------------------------
        //
        //  Variables
        //
        //---------------------------------------------------------------------
        protected var socket : Socket;

        protected var state : int;

        public var applicationID : int = 0;

        public var curPacket : ESPacket;

        public var socketWaiting : int;

        /**
         * This contains a 'true' for each clientID that is already connected.
         */
        protected var clients : Dictionary = new Dictionary(false);

        public var serverID : int;

        public var host : String;

        public var port : int;

        //---------------------------------------------------------------------
        //
        //  Properties
        //
        //---------------------------------------------------------------------
        private var _connected : Boolean = false;

        public function get connected() : Boolean
        {
            return _connected;
        }

        public function get authenticated() : Boolean
        {
            return connected && clientID != 0;
        }

        private var _clientID : int = 0;

        public function get clientID() : int
        {
            return _clientID;
        }

        public function get isServer() : Boolean
        {
            return serverID == clientID;
        }

        //---------------------------------------------------------------------
        //
        //  Constructor
        //
        //---------------------------------------------------------------------
        /**
         *  Constructor
         */
        public function ESSocket(host : String, port : int, applicationID : int = 0)
        {
            super();

            this.applicationID = applicationID;
            this.host = host;
            this.port = port;

            socket = new Socket();
            socket.endian = Endian.LITTLE_ENDIAN;

            socket = new Socket();
            socket.endian = Endian.LITTLE_ENDIAN;
            socket.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
            socket.addEventListener(Event.CONNECT, connectHandler);
            socket.addEventListener(ProgressEvent.SOCKET_DATA, socketDataHandler);
        }

        //---------------------------------------------------------------------
        //
        //  Methods
        //
        //---------------------------------------------------------------------
        public function connect() : void
        {
            socket.connect(host, port);
        }

        public function close() : void
        {
            if (connected && socket.connected)
            {
                socket.close();
                _connected = false;
            }
        }

        /**
         * Sends a packet to the clientID provided.
         *
         * If the clientID is 0, then this sends to all peers. If clientID is greater
         * than 0, the packet is sent to a specific peer.
         */
        public function sendPacket(packet : ESPacket) : void
        {
            if (!authenticated || !packet.data || packet.data.length == 0)
            {
                return;
            }

            packet.data.position = 0;

            var ba : ByteArray = new ByteArray();
            ba.endian = Endian.LITTLE_ENDIAN;

            // Prepend the destination clientID... if 0 then it means
            // to send to everyone.
            ba.writeByte(packet.clientID);
            ba.writeBytes(packet.data, 0, packet.data.length);
            ba.position = 0;

            socket.writeBytes(ba, 0, ba.bytesAvailable);

            socket.flush();
        }

        public function resetToStatusCode() : void
        {
            // We need to read 1 byte: the status code
            socketWaiting = 1;
            state = STATE_STATUS_BYTE;
        }

        protected function readPacketHeader() : void
        {
            var sourceID : int = socket.readByte();

            curPacket = new ESPacket(new ByteArray(), sourceID);
            curPacket.data.endian = Endian.LITTLE_ENDIAN;
            socketWaiting = socket.readShort();
            state = STATE_PACKET;

            if (!clients[sourceID])
            {
                clients[sourceID] = true;

                dispatchEvent(new ESJoinEvent(sourceID));
            }
        }

        protected function readClientPacket() : void
        {
            // Okay, if we hit here we know we have all the data, so 
            // put it into the current packet and emit it
            socket.readBytes(curPacket.data, 0, socketWaiting);
            curPacket.data.position = 0;

            dispatchEvent(new ESPacketEvent(curPacket));

            resetToStatusCode();
        }

        protected function readDeath() : void
        {
            var cID : int = socket.readByte();

            delete clients[cID];

            dispatchEvent(new ESLeaveEvent(cID));

            resetToStatusCode();
        }

        /**
         * The server has died. Next byte is the ID of the new
         * server, which might be us.
         */
        protected function readServerDeath() : void
        {
            delete clients[serverID];

            dispatchEvent(new ESLeaveEvent(serverID));

            serverID = socket.readByte();

            if (isServer)
            {
                dispatchEvent(new ESServerEvent(ESServerEvent.START_SERVING));
            }
            else
            {
                dispatchEvent(new ESServerEvent(ESServerEvent.SERVER_CHANGED));
            }

            resetToStatusCode();
        }

        /**
         * Reads in the clientID of this client and the serverID. This completes
         * the initialization so we send out the appropriate CONNECT event.
         */
        protected function readClientID() : void
        {
            _clientID = socket.readByte();
            serverID = socket.readByte();

            resetToStatusCode();

            dispatchEvent(new ESCAuthenticateEvent(_clientID, isServer));
        }

        /**
         * Reads in the byte of status code and responds accordingly.
         */
        protected function readStatusCode() : void
        {
            var stat : int = socket.readByte();

            switch (stat)
            {
                // This indicates a standard packet is on the line.
                case 1:
                    state = STATE_PACKET_HEADER;

                    // We are now waiting for 1 byte that is a client ID
                    // and then two socket that is the size we are expecting.
                    socketWaiting = 3;
                    break;
                // This indicates a peer death.
                case 2:
                    state = STATE_PEER_DEATH;
                    // The next byte will be the ID of the peer that died.
                    socketWaiting = 1;
                    break;
                case 3:
                    state = STATE_SERVER_DEATH;
                    // The next byte will be the ID of the new server.
                    socketWaiting = 1;
                    break;
                default:
                    trace("Socket reading is out of sync.");
                    resetToStatusCode();
            }
        }

        public function read() : void
        {
            while (connected && socket.connected && socket.bytesAvailable >= socketWaiting)
            {
                switch (state)
                {
                    // READING CLIENT ID
                    case STATE_INIT:
                        readClientID();
                        break;
                    // READING STATUS CODE
                    case STATE_STATUS_BYTE:
                        readStatusCode();
                        break;
                    // READING DEATH ID
                    case STATE_PEER_DEATH:
                        readDeath();
                        break;
                    case STATE_SERVER_DEATH:
                        readServerDeath();
                        break;
                    case STATE_PACKET_HEADER:
                        readPacketHeader();
                        break;
                    // READING OTHER CLIENT STATE
                    case STATE_PACKET:
                        readClientPacket();
                        break;
                }
            }
        }

        //---------------------------------------------------------------------
        //
        //  Events
        //
        //---------------------------------------------------------------------
        /**
         * @private
         */
        protected function socketDataHandler(event : ProgressEvent) : void
        {
            if (connected && socket.connected)
            {
                read();
            }
        }

        /**
         * @private
         */
        protected function ioErrorHandler(event : IOErrorEvent) : void
        {
            _connected = false;
        }

        /**
         * @private
         */
        protected function connectHandler(event : Event) : void
        {
            socket.writeByte(applicationID);
            socket.flush();

            socketWaiting = 2;
            state = STATE_INIT;

            _connected = true;
        }
    }
}
